﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Windows;
using System.Windows.Browser;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

using Microsoft.Xna.Framework;

using XnaDevRu.BulletX;
using XnaDevRu.BulletX.Dynamics;

namespace SilverlightPhysics
{
    public partial class Page : Canvas
    {
        /// <summary>
        /// Recycler queue
        /// </summary>
        private class RecyclerQueue : Queue<Primitive>
        {
        }

        /// <summary>
        /// Type recycler
        /// </summary>
        private class Recycler : Dictionary<Type, RecyclerQueue>
        {
        }

        private DateTime last = DateTime.Now;
        private World world = new World();
        private Random rand = new Random();
        private Recycler recycler = new Recycler();

        /// <summary>
        /// Page loaded
        /// </summary>
        public void Page_Loaded( object o, EventArgs e )
        {
            // Required to initialize variables
            InitializeComponent();
            Initialize();

            BrowserHost.Resize += new EventHandler( BrowserHost_Resize );

            StartTimer();
        }

        private void StartTimer()
        {
            ( Resources.FindName( "timer" ) as Storyboard ).Begin();
        }

        /// <summary>
        /// Handle the resize event
        /// </summary>
        void BrowserHost_Resize( object sender, EventArgs e )
        {
            Width = BrowserHost.ActualWidth;
            Height = BrowserHost.ActualHeight;

            // Scale the content
            ScaleTransform scale = FindName( "scale" ) as ScaleTransform;
            scale.ScaleX = BrowserHost.ActualHeight / 20;
            scale.ScaleY = BrowserHost.ActualHeight / 20;

            // Keep it centered
            TranslateTransform translate = FindName( "translate" ) as TranslateTransform;
            translate.X = BrowserHost.ActualWidth / 2;
            translate.Y = BrowserHost.ActualHeight / 2;
        }

        /// <summary>
        /// Initialize the world
        /// </summary>
        private void Initialize()
        {
            world.Gravity = new Vector3( 0, -30, 0 );

            AddBox( new Vector3( 0, -8, 0 ), new Vector3( 10, 0.2f, 10 ), 0 );

            dropSphere_Click( this, EventArgs.Empty );
        }

        /// <summary>
        /// Step the simulation and render the scene on the timer tick
        /// </summary>
        void timer_Tick( object sender, EventArgs e )
        {
            DateTime now = DateTime.Now;
            double ms = ( now - last ).TotalMilliseconds;
            World.StepSimulation( (float)ms );
            last = now;

            Canvas canvas = FindName( "content" ) as Canvas;
            StringBuilder status = new StringBuilder();
            List<CollisionObject> removed = new List<CollisionObject>();

            foreach ( CollisionObject collision in world.CollisionObjects )
            {
                Render( canvas, collision, removed, status );
            }

            // TODO: We really need this to make sure CollisionObjects are removed from the physics world.  Unfortunately
            // there seems to be a bug in the physics engine (simple 'index out of bounds' error) that occasionally shows
            // up when you start removing items.  It's on my todo list ;o)
            /*
            foreach ( CollisionObject collision in removed )
            {
                World.RemoveCollisionObject( collision );
            }
            */

            // FindName( "pos" ).SetValue( TextBlock.TextProperty, status.ToString() );
            FindName( "pos" ).SetValue( TextBlock.TextProperty, string.Format( "Recycler: {0}  FPS: {1:F0}",
                GetRecyclerQueue( typeof( SphereShape ) ).Count,
                1000 / ms ) );

            StartTimer();
        }

        /// <summary>
        /// Render an item
        /// </summary>
        /// <param name="canvas">Target canvas</param>
        /// <param name="collision">Collision object (rigid body)</param>
        /// <param name="removed">This collection receives a list of items to remove from the scene</param>
        /// <param name="status">Used for debugging</param>
        /// <returns>True if object still in world</returns>
        private bool Render( Canvas canvas, CollisionObject collision, List<CollisionObject> removed, StringBuilder status )
        {
            RigidBody body = RigidBody.Upcast( collision );
            Microsoft.Xna.Framework.Matrix worldMatrix;

            if ( body != null && body.MotionState != null )
            {
                DefaultMotionState motionState = body.MotionState as DefaultMotionState;

                if ( !body.IsStaticObject )
                {
                    // Constrain rotation to the Z axis
                    Microsoft.Xna.Framework.Matrix m;
                    motionState.GetWorldTransform( out m );
                    motionState.SetWorldTransform( new Microsoft.Xna.Framework.Matrix(
                                                   (float)m.M11, (float)m.M12, 0, 0, (float)m.M21, (float)m.M22, 0, 0, 0, 0, 1, 0, (float)m.M41, (float)m.M42, 0, 0 ) );
                    body.AngularVelocity = new Vector3( 0, 0, body.AngularVelocity.Z );
                }

                worldMatrix = motionState.GraphicsWorldTransform;
            }
            else
            {
                worldMatrix = collision.WorldTransform;
            }

            // Get the display matrix
            // TODO: Find out why we need to transpose
            Microsoft.Xna.Framework.Matrix displayMatrix = worldMatrix;
            displayMatrix.Translation = Vector3.Zero;
            displayMatrix = Microsoft.Xna.Framework.Matrix.Transpose( displayMatrix );
            displayMatrix.Translation = worldMatrix.Translation;

            Primitive primitive = collision.UserData as Primitive;
            if ( primitive == null ) return false;

            // If the item falls "off the bottom" then remove it
            if ( displayMatrix.Translation.Y < -20 )
            {
                GetRecyclerQueue( collision.CollisionShape.GetType() ).Enqueue( primitive );

                if ( primitive.Element != null ) primitive.Element.Visibility = Visibility.Hidden;

                collision.UserData = null;
                collision.ActivationState = ActivationState.Nothing;
                removed.Add( collision );

                return false;
            }

            UIElement element = primitive.Element;

            // Create the visual element if not already done
            if ( element == null )
            {
                if ( primitive.ResourceName == "sphere" )
                {
                    EllipseBody ellipse = new EllipseBody();
                    ellipse.Width = 1;
                    ellipse.Height = 1;
                    element = ellipse;
                }
                else
                {
                    Rectangle rectangle = new Rectangle();
                    rectangle.Width = 1;
                    rectangle.Height = 1;
                    rectangle.Fill = primitive.Brush;
                    element = rectangle;
                }

                canvas.Children.Add( element );

                primitive.Element = element;
            }
            else
            {
                element.Visibility = Visibility.Visible;
            }

            // Determine the size

            ScaleTransform scale = new ScaleTransform();

            BoxShape box = collision.CollisionShape as BoxShape;
            SphereShape sphere = collision.CollisionShape as SphereShape;

            if ( box != null )
            {
                scale.ScaleX = box.HalfExtents.X * 2;
                scale.ScaleY = box.HalfExtents.Y * 2;
            }
            else if ( sphere != null )
            {
                scale.ScaleX = sphere.Radius * 2;
                scale.ScaleY = sphere.Radius * 2;
            }

            // Get the rotation around the Z axis from the matrix

            RotateTransform rotate = new RotateTransform();
            float angle = (float)( -Math.Atan2( displayMatrix.M12, displayMatrix.M11 ) / Math.PI * 180 );

            rotate.Angle = angle;
            rotate.CenterX = scale.ScaleX / 2;
            rotate.CenterY = scale.ScaleY / 2;

            // Get the translation from the matrix

            TranslateTransform translate = new TranslateTransform();
            translate.X = -( scale.ScaleX / 2 ) + displayMatrix.Translation.X;
            translate.Y = -( scale.ScaleY / 2 ) - displayMatrix.Translation.Y;

            // Group and apply the transforms

            TransformGroup transform = new TransformGroup();
            transform.Children.Add( scale );
            transform.Children.Add( rotate );
            transform.Children.Add( translate );

            element.RenderTransform = transform;

            return true;
        }

        /// <summary>
        /// Get the physics world
        /// </summary>
        private World World
        {
            get { return world; }
        }

        /// <summary>
        /// Get the RecyclerQueue for the specified Type
        /// </summary>
        /// <param name="type">Type to retrieve queue for</param>
        /// <returns>RecyclerQueue for Type</returns>
        private RecyclerQueue GetRecyclerQueue( Type type )
        {
            if ( recycler.ContainsKey( type ) ) return recycler[ type ];

            RecyclerQueue queue = new RecyclerQueue();
            recycler.Add( type, queue );
            return queue;
        }

        /// <summary>
        /// Get a recycled primitive
        /// </summary>
        /// <param name="type">Type identifier</param>
        /// <returns>A recycled Primitive, or null if none available</returns>
        private Primitive Recycle( Type type )
        {
            RecyclerQueue queue = GetRecyclerQueue( type );
            return ( queue.Count > 0 ) ? queue.Dequeue() : null;
        }

        /// <summary>
        /// Create a rigid body
        /// </summary>
        /// <param name="position">Position</param>
        /// <param name="shape">Collision shape</param>
        /// <param name="mass">Relative mass</param>
        /// <returns>New rigid body</returns>
        private RigidBody CreateRigidBody( Vector3 position, CollisionShape shape, double mass )
        {
            Microsoft.Xna.Framework.Vector3 inertia = new Vector3();
            if ( mass != 0 ) shape.CalculateLocalInertia( (float)mass, out inertia );

            Microsoft.Xna.Framework.Matrix trans = Microsoft.Xna.Framework.Matrix.Identity;
            trans.Translation = position;

            Microsoft.Xna.Framework.Matrix center = Microsoft.Xna.Framework.Matrix.Identity;

            MotionState motion = new DefaultMotionState( trans, center );

            return new RigidBody( (float)mass, motion, shape, inertia, 0.5f, 0.5f, 0.5f, 0.5f );
        }

        /// <summary>
        /// Add a box to the physics world
        /// </summary>
        /// <param name="position">Position</param>
        /// <param name="halfExtents">Half extents for box</param>
        /// <param name="mass">Relative mass</param>
        /// <returns>New box</returns>
        private CollisionObject AddBox( Vector3 position, Vector3 halfExtents, double mass )
        {
            RigidBody body = CreateRigidBody( position, new BoxShape( halfExtents ), mass );

            Brush brush = new SolidColorBrush( Colors.Gray );

            if ( mass == 0 ) brush = new SolidColorBrush( Colors.Brown );
            else
            {
                switch ( rand.Next( 3 ) )
                {
                    case 0: brush = new SolidColorBrush( Colors.Red ); break;
                    case 1: brush = new SolidColorBrush( Colors.Blue ); break;
                    case 2: brush = new SolidColorBrush( Colors.Green ); break;
                }
            }

            body.UserData = Recycle( typeof( BoxShape ) );
            if ( body.UserData == null ) body.UserData = new Primitive( "box", brush );

            World.AddRigidBody( body );

            return body;
        }

        /// <summary>
        /// Add a sphere to the physics world
        /// </summary>
        /// <param name="position">Position</param>
        /// <param name="radius">Sphere radius</param>
        /// <param name="mass">Relative mass</param>
        /// <returns>New sphere</returns>
        private CollisionObject AddSphere( Vector3 position, double radius, double mass )
        {
            RigidBody body = CreateRigidBody( position, new SphereShape( (float)radius ), mass );

            body.UserData = Recycle( typeof( SphereShape ) );
            if ( body.UserData == null ) body.UserData = new Primitive( "sphere", new SolidColorBrush( Colors.Red ) );

            World.AddRigidBody( body );

            return body;
        }

        /// <summary>
        /// Drop a new sphere
        /// </summary>
        private void dropSphere_Click( object sender, EventArgs args )
        {
            double radius = 0.5 + ( rand.NextDouble() * 0.5 );
            AddSphere( new Vector3( (float)( ( rand.NextDouble() * 0.2 ) - 0.1 ), 3, 0 ), radius, radius * 100 );
        }

        /// <summary>
        /// Drop a new box
        /// </summary>
        private void dropBox_Click( object sender, EventArgs args )
        {
            Vector3 halfExtents = new Vector3(
                                      (float)( 0.5 + ( rand.NextDouble() * 0.5 ) ),
                                      (float)( 0.5 + ( rand.NextDouble() * 0.5 ) ),
                                      (float)( 0.5 + ( rand.NextDouble() * 0.5 ) ) );

            AddBox(
                new Vector3( (float)( ( rand.NextDouble() * 0.2 ) - 0.1 ), 3, 0 ),
                halfExtents,
                halfExtents.X * 100 );
        }
    }
}